Skip to content

Fix: declare the four hoisted runtime dependencies in the dev workspace - #577

Open
AmaadMartin wants to merge 3 commits into
mainfrom
fix/dev-workspace-undeclared-dependencies
Open

Fix: declare the four hoisted runtime dependencies in the dev workspace#577
AmaadMartin wants to merge 3 commits into
mainfrom
fix/dev-workspace-undeclared-dependencies

Conversation

@AmaadMartin

@AmaadMartin AmaadMartin commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

  1. Link to an existing issue (if applicable):
    N/A — no public issue is open for this.

  2. Or, if no issue exists, describe the change:

Problem: @google/adk-devtools (the dev workspace) imports five npm packages that its own dev/package.json never declares:

Package Import sites Kind
@opentelemetry/api dev/src/utils/telemetry_utils.ts:13 (HrTime), dev/src/server/adk_api_server.ts:29 (trace, TracerProvider) trace is a runtime value (adk_api_server.ts:160); the rest are types that reach the emitted .d.ts
@opentelemetry/sdk-trace-base dev/src/utils/telemetry_utils.ts:14-18, dev/src/server/adk_api_server.ts:30 (SimpleSpanProcessor) SimpleSpanProcessor is a runtime value (adk_api_server.ts:153-154)
lodash-es dev/src/integration/test_runner.ts:15 (cloneDeep) runtime value (test_runner.ts:68)
@google-cloud/vertexai dev/src/cli/deploy/cli_deploy_agent_engine.ts:9-10 Client is a runtime value (:137)

They resolve inside the monorepo only because core/package.json declares them and npm hoists them to the repo-root node_modules. A user running npm install @google/adk-devtools outside this repo gets none of them.

The failure is invisible in-repo, which is why it survived. I verified the mechanism rather than assuming it: dev/build.js passes packages: 'external' to esbuild, so nothing is bundled. Counting specifier occurrences in the packed tarball's output confirms every one survives to be resolved against the consumer's tree:

@google/genai                    js:6  dts:6
@opentelemetry/api               js:4  dts:4
@opentelemetry/sdk-trace-base    js:3  dts:3
lodash-es                        js:2  dts:2
@google-cloud/vertexai           js:2  dts:2

Solution: Declare all five in dev/package.json, copying each range character for character from core/package.json (the source of truth) so npm keeps collapsing both workspaces onto one physical copy:

"@google-cloud/vertexai": "^1.12.0",   // core/package.json:47
"@opentelemetry/api": "1.9.0",         // core/package.json:52 — exact pin, no caret
"@opentelemetry/sdk-trace-base": "^2.1.0", // core/package.json:61
"lodash-es": "^4.18.1",                // core/package.json:68
"@google/genai": "^2.9.0"              // core/package.json:48

All five go in dependencies, not devDependencies: each contributes a runtime value to published output, and the OTel types additionally appear in dev's emitted .d.ts — verified in the packed tarball, e.g. dist/types/server/adk_api_server.d.ts:7 imports TracerProvider.

"@opentelemetry/api" is deliberately left un-caretted to match core's pin. This is load-bearing, not cosmetic: two copies of the OTel API in one process each carry their own global tracer registry, and spans recorded against one are invisible to the other. A test pins this exact-pin invariant (mutation 2 below).

A fifth manifest line beyond the four audited packages: @types/lodash-es: ^4.17.12 in devDependencies. lodash-es@4.18.1 ships no type declarations of its own, so dev's tsc --emitDeclarationOnly build step cannot typecheck test_runner.ts without it. It is a devDependency because cloneDeep's types never surface in dev's public .d.ts — mirroring how core pairs the two (core/package.json:68 + :77).

Secondary change — Client moved to the package root. Being precise about what this does and does not achieve, since it is the one hunk in this PR that is not strictly required to declare a dependency:

  • It is not a resolution fix. @google-cloud/vertexai@1.12.0 has no exports map at all, so the deep path build/src/genai/client.js resolved fine before and would continue to, hoisted or not. Nothing was broken.
  • It is a repo-guideline compliance change: "Use Clean Imports for SDKs: Import main classes directly from the root package (e.g. @google-cloud/vertexai) instead of deep paths." build/src/index.d.ts does export { Client } from './genai/client', so the root exposes Client, and all three Client importers in core already use it. Both specifiers resolve to the same file in the same package instance, so class identity is unchanged.
  • It reduces but does not eliminate this file's coupling to vertexai internals: line 10 still deep-imports ReasoningEngine, which is declared in build/src/genai/types/common.d.ts and re-exported only by build/src/genai/types.d.ts — the package root genuinely does not expose it. Two deep imports become one. If you would rather this PR stayed purely about manifests, these two hunks are separable and can be dropped without affecting anything else here; Fix: drop @google-cloud/vertexai deep build-output imports from the Agent Engine deploy CLI #279 also covers this rewrite.

One-line mock retarget, and why it is not optional. Vitest intercepts by specifier, so once the source imports the root, the deep-path vi.mock at dev/test/cli/cli_deploy_agent_engine_test.ts:144 silently stops applying and the suite constructs the real Client. I retargeted that one specifier and left the factory body and every assertion untouched. This is a mechanical consequence of the source change, not a rewrite of assertions — and mutation 3 below shows it is load-bearing rather than cosmetic: leaving the mock on the deep path fails 12 of 17 tests with live 403s against the Agent Platform API.

Collision check (done before writing any code). gh pr list --repo AmaadMartin/adk-js --state open --limit 1000 (the fork has ~380 open PRs; smaller limits silently truncate). Adjacent open PRs found, and how this one relates:

I initially stopped as a duplicate; that was overruled on the grounds that none of the above is a byte-exact match for this change's scope. Whoever merges these should expect a textual conflict in the alphabetically-sorted dependencies block and in package-lock.json, resolvable by keeping both sets of keys in alphabetical order and re-running npm install.

Scope note — @google/genai is now included. The original plan for this change deliberately excluded it, on the grounds that a sibling PR (#244) owns it. A complexity review pushed back on that, correctly: leaving it out shipped a fix that did not actually achieve the stated goal. It is an undeclared runtime dependency of exactly the same kind — 6 import sites, createUserContent called at dev/src/server/adk_api_client.ts:153 (and present in the published output as import_genai.createUserContent), reached through AdkApiClient, which is a public export at dev/src/index.ts:7. A standalone install got ERR_MODULE_NOT_FOUND on a public entry point. Declaring four of five packages would have left the bug live while looking fixed, so it is declared here at core's ^2.9.0.

A repo-wide phantom-dependency guard is still not part of this PR — that is a different mechanism (walking every workspace's src and diffing against its manifest) and is already open separately as #450 and #250. The list in package_manifest_test.ts is hand-maintained and covers what this PR is responsible for; the same review noted a hardcoded list can go stale, which is precisely what the automated guard in #450 is for.

Resolved versions after npm install (read out of package-lock.json; npm ls reports every one as deduped, i.e. core and dev share the single hoisted root copy rather than each having their own):

Package Resolved Location
@opentelemetry/api 1.9.0 root node_modules (shared by root, core, dev)
@opentelemetry/sdk-trace-base 2.8.0 root node_modules (shared by root, core, dev)
lodash-es 4.18.1 root node_modules (shared by root, core, dev)
@google-cloud/vertexai 1.12.0 root node_modules (shared by root, core, dev)
@types/lodash-es 4.17.12 root node_modules (shared by root, core, dev)

npm install created no new nesting: the package-lock.json diff is confined to the six added keys inside the "dev" entry under "packages", and adds/changes zero node_modules/... entries. (Pre-existing nested copies that are other packages' own pins are untouched: @opentelemetry/sdk-trace-base@2.1.0 under @opentelemetry/otlp-transformer and @opentelemetry/exporter-trace-otlp-http, and @google/genai@1.52.0 under @google-cloud/vertexai. Deduping that last one is its own concern, open separately as #274/#515.)

No behavioral change to any shipped code path. Not a breaking change — packages that previously had to be present by accident are now requested explicitly, and every declared range is already satisfied by the installed copy, so existing monorepo users see no version movement.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.

Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.

New test dev/test/package_manifest_test.ts (6 cases) pins each range against core's. It resolves both manifests from import.meta.url rather than process.cwd(), so it does not depend on vitest running from the repo root, and it asserts core's value is toBeDefined() before the equality assertion — otherwise undefined === undefined would pass vacuously if both manifests lost the key.

$ npx vitest run --project unit:dev dev/test/package_manifest_test.ts dev/test/cli/cli_deploy_agent_engine_test.ts
 ✓ |unit:dev| dev/test/package_manifest_test.ts (6 tests) 6ms
 ✓ |unit:dev| dev/test/cli/cli_deploy_agent_engine_test.ts (17 tests) 49ms
 Test Files  2 passed (2)
      Tests  23 passed (23)

This change adds zero new executable lines under any */src/**, so the v8 coverage include globs see no new code and the global thresholds in vitest.config.ts are unaffected. No threshold was lowered or adjusted.

Proof each test can fail. Coverage is not proof, so I ran four mutations and confirmed each produces a real failure:

  1. Deleted "@opentelemetry/api": "1.9.0" from dev/package.json:
    × declares @opentelemetry/api as a runtime dependency matching core
      → AssertionError: expected undefined to be '1.9.0' // Object.is equality
      Tests  1 failed | 4 passed (5)
    
  2. Added a caret — "^1.9.0" — proving the test pins the exact-pin invariant and not merely presence:
    × declares @opentelemetry/api as a runtime dependency matching core
      → AssertionError: expected '^1.9.0' to be '1.9.0' // Object.is equality
      Tests  1 failed | 4 passed (5)
    
  3. Reverted the source import to the deep path while leaving the vi.mock on the root, to prove the mock retarget is load-bearing. 12 of 17 tests failed and the suite tried to reach the live Agent Platform API, exactly the silent-passthrough hazard the retarget prevents:
    ApiError: {"error":{"code":403,"message":"Agent Platform API has not been used in
    project test-project before or it is disabled...","status":"PERMISSION_DENIED"}}
    AssertionError: expected [Function] to throw error including 'Reasoning Engine update
    failed: [Code…' but got '{"error":{"code":403,...'
    TypeError: Cannot read properties of undefined (reading 'Symbol(client)')
    
  4. Deleted "@google/genai": "^2.9.0":
    × declares @google/genai as a runtime dependency matching core
      → AssertionError: expected undefined to be '^2.9.0' // Object.is equality
      Tests  1 failed | 5 passed (6)
    
    All four mutations were reverted and the suites re-run green.

No existing test was deleted, skipped, weakened, or had an assertion changed.

Manual End-to-End (E2E) Tests:

Packaging check — the actual user-visible consequence:

$ npm pack --workspace dev && tar -xzf google-adk-devtools-1.5.0.tgz
$ node -e "…read package/package.json…"
dependencies.@google-cloud/vertexai        = ^1.12.0
dependencies.@google/genai                 = ^2.9.0
dependencies.@opentelemetry/api            = 1.9.0
dependencies.@opentelemetry/sdk-trace-base = ^2.1.0
dependencies.lodash-es                     = ^4.18.1
devDependencies.@types/lodash-es           = ^4.17.12

Repo-root verification:

$ npm install                     # lockfile diff confined to the "dev" block; 0 node_modules/... entries
$ ls dev/node_modules             # no nested copy of any of the six
$ npm ls @opentelemetry/api @opentelemetry/sdk-trace-base lodash-es @google-cloud/vertexai \
         @types/lodash-es @google/genai   # every entry "deduped" onto the root copy
$ npm run build                   # PASS (includes dev's tsc --emitDeclarationOnly)
$ npx secretlint <changed files>  # PASS
$ npm run lint                    # PASS
$ npm run format:check            # PASS — "All matched files use Prettier code style!"
$ npm run docs:check              # PASS

One honest caveat: npm run ts:check exits 2, but it does so identically on the clean baseline. I verified this rather than assuming — stashing this change and re-running produces a byte-identical 41-line error list, none of it in any file this PR touches. It is a pre-existing repo condition (and is not one of the CI steps in .github/workflows/validation.yaml, which runs npm install, secretlint, build, test:coverage, lint, format:check, docs:check).

CI status (reported honestly)

Head commit 37aaee66. Four of five checks green; run-tests (windows-latest) is red and I am not calling this run green.

Job Result
check-license pass
run-tests pass
run-tests (ubuntu-latest) pass — 225 files, 0 failed, incl. ✓ dev/test/package_manifest_test.ts (6 tests)
run-tests (macos-latest) pass (6m4s)
run-tests (windows-latest) fail — 1 file, 224 passed: core/test/code_executors/unsafe_local_code_executor_test.ts > UnsafeLocalCodeExecutor > should execute shell code and return stdout, Error: Test timed out in 5000ms

Both failures seen on this branch are pre-existing CI flakes, not regressions from this change. I verified that rather than asserting it:

I deliberately have not touched those suites to force a green tick. Raising their timeouts is a separate concern with existing PRs, and bundling it would add unrelated churn to a dependency-declaration diff. Local validation on this exact commit: 23/23 targeted tests, npm run build, lint, format:check, docs:check and secretlint all pass.

Checklist

[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.

Amaad Martin added 3 commits August 3, 2026 08:15
dev/src imports @opentelemetry/api, @opentelemetry/sdk-trace-base,
lodash-es and @google-cloud/vertexai, but dev/package.json declares
none of them. They resolve in-repo only because core/package.json
declares them and npm hoists them to the workspace root, so
'npm install @google/adk-devtools' outside this repo gets none of
them. dev/build.js passes packages:'external' to esbuild, so every
one of these specifiers survives unbundled into the published
dist/esm and dist/cjs and is resolved against the consumer's tree.

Each range is copied character for character from core/package.json
so npm keeps collapsing both workspaces onto one physical copy.
@opentelemetry/api stays an exact pin: two copies of the OTel API in
one process each carry their own global tracer registry, and spans
recorded against one are invisible to the other.

@types/lodash-es is a devDependency because lodash-es ships no type
declarations and dev's 'tsc --emitDeclarationOnly' build step needs
them, but cloneDeep's types never reach dev's public .d.ts. This
mirrors how core pairs the two.
@google-cloud/vertexai@1.12.0 re-exports Client from its root
(build/src/index.d.ts), so the deep build-output path reached into
implementation detail with no semver guarantee. Both specifiers
resolve to the same file in the same package instance, so class
identity is unchanged.

ReasoningEngine stays on its deep path: it is declared in
build/src/genai/types/common.d.ts and re-exported only by
build/src/genai/types.d.ts, so the package root does not expose it.

Vitest intercepts by specifier, so the deep-path vi.mock in
cli_deploy_agent_engine_test.ts would have silently stopped applying
and let the suite construct the real Client; the mock is retargeted
to match. Its factory body is unchanged.
dev/src imports @google/genai at six sites and it was left undeclared,
resolving only by hoisting from core -- the same bug class this branch
fixes for the other four packages, so leaving it out shipped a fix that
did not actually make dev standalone-installable.

It is not type-only: createUserContent is called at
dev/src/server/adk_api_client.ts:153 and survives into the published
output as import_genai.createUserContent, and AdkApiClient is exported
from dev/src/index.ts, so a standalone install hit ERR_MODULE_NOT_FOUND
on a public entry point.

The range matches core/package.json:48 so npm keeps both workspaces on
one copy; npm ls confirms @google/genai@2.9.0 deduped for core and dev.
The nested 1.52.0 under @google-cloud/vertexai is that package's own
pin and is untouched.

Added to SHARED_RUNTIME_DEPENDENCIES so the guard covers it: the list
is hand-maintained, so an omission there is invisible.
AmaadMartin pushed a commit that referenced this pull request Aug 4, 2026
Replaces the Math.random() UUID fallback with crypto.getRandomValues(), and throws rather than silently degrading to a non-cryptographic generator when no secure source exists.

crypto.randomUUID() is secure-context-only, so it is absent on plain-HTTP origins even where crypto is present; getRandomValues() carries no such restriction and is used as the fallback. Callers making security decisions on this value (the OAuth2 state parameter in AuthHandler, and session identifiers minted by the session services) can no longer be handed a predictable UUID.

Fallback applies RFC 4122 section 4.4 version and variant bits and zero-pads every byte. Tests pin the randomUUID branch discriminatingly, cover the getRandomValues fallback deterministically, and assert the throw.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant